package com.fastjars.mybatis.dynamic;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * DynamicPlugin
 *
 * @author Brant Liu <br> 邮箱：<br>lbf1988@qq.com <br>日期：<br>2018/10/10
 * @version 1.0.0
 */
@Intercepts(value = {
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })
})
public class DynamicPlugin implements Interceptor {
    static final Logger LOGGER = LoggerFactory.getLogger(DynamicPlugin.class);

    /**
     * 增删改使用主库操作
     */
    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";

    private static final Map<String, DBType> cacheMap = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
        if(!synchronizationActive) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];

            DBType dbType = cacheMap.get(ms.getId());

            if(null == dbType){
                // TODO: 2018/10/10 获取方法中定义的目标数据源
                dbType = getDefinedAnnotationTargetDBSource(ms);
            }

            if(null == dbType) {
                // TODO: 2018/10/10 如果数据类型是SELECT语句 或者 不是为自增id查询主键(SELECT LAST_INSERT_ID() )方法
                if(ms.getSqlCommandType().equals(SqlCommandType.SELECT) && !ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                    BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                    String executeSql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                    if(executeSql.matches(REGEX)) {
                        dbType = DBType.MASTER;
                    } else {
                        dbType = DBType.SLAVE;
                    }
                } else {
                    dbType = DBType.MASTER;
                }
            }

            if(null == dbType){
                throw new IllegalArgumentException("Target datasource must be not null");
            }

            LOGGER.info("Set method [{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dbType.name(), ms.getSqlCommandType().name());
            cacheMap.put(ms.getId(), dbType);

            DBHolder.putDataSource(dbType);
        }
        return invocation.proceed();
    }

    /**
     * 获取方法定义的目标数据源
     * @param ms
     * @return
     */
    public DBType getDefinedAnnotationTargetDBSource(MappedStatement ms){
        String id = ms.getId();
        int lastCharIndex = id.lastIndexOf(".");
        String className = id.substring(0, lastCharIndex);
        String methodName = id.substring(lastCharIndex + 1, id.length());
        try {
            Method method = ClassUtils.getMethod(Class.forName(className),methodName,null);
            // TODO: 2018/10/10 获取目标数据源
            TargetDBSource targetDBSource = method.getAnnotation(TargetDBSource.class);
            if(null != targetDBSource && null != targetDBSource.value()){
                return targetDBSource.value();
            }
        } catch (Exception ex) {
            LOGGER.error("Exception:{}",ex.getMessage());
        }
        return null;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
